--- title: "01-计划" created: 2025-11-28 aliases: - 计划 tags: - 项目 --- # 计划 ## **第 1 周:基础架构 + 微服务搭建** **学习目标:** 项目脚手架、Nacos、数据库设计、基础 CRUD **核心任务:** ```text 1. 创建 Maven 多模块项目 2. 集成 Spring Cloud 框架 3. 设计核心数据库表(用户、创意、拍卖、交易等) 4. 实现用户服务和认证模块 5. 所有微服务注册到 Nacos 6. 配置 API Gateway ``` **交付物:** - ✅ 完整的多模块项目结构 - ✅ 用户注册/登录/认证功能 - ✅ 所有服务能通过 Nacos 发现 --- ## **第 2 周:创意服务 + 缓存优化** **学习目标:** Redis 多级缓存、布隆过滤器、ElasticSearch 搜索 **核心任务:** ```text 1. 实现创意作品的 CRUD(发布、编辑、删除) 2. 集成 Redis + Caffeine 多级缓存 - 热门创意缓存 - 搜索结果缓存 3. 布隆过滤器防止缓存穿透 4. ElasticSearch 全文搜索集成 5. 缓存预热任务 ``` **重点代码示例:** ```java // 创意详情查询 - 三层缓存防护 public Artwork getArtworkDetail(Long artworkId) { // 第1层:本地缓存(Caffeine) Artwork cached = localCache.getIfPresent(artworkId); if (cached != null) { return cached; } // 第2层:布隆过滤器检查数据是否存在 if (!bloomFilter.contains(artworkId)) { // 数据不存在,直接返回 null return null; } // 第3层:Redis 分布式缓存 String key = "artwork:" + artworkId; Artwork redisData = redisTemplate.opsForValue() .get(key); if (redisData != null) { localCache.put(artworkId, redisData); return redisData; } // 都没有 → 查数据库(加分布式锁防击穿) RLock lock = redisson.getLock("artwork:lock:" + artworkId); try { if (!lock.tryLock(2, 10, TimeUnit.SECONDS)) { // 加锁失败,等待后重试 Thread.sleep(50); return getArtworkDetail(artworkId); } Artwork dbData = artworkMapper.selectById(artworkId); if (dbData != null) { redisTemplate.opsForValue() .set(key, dbData, Duration.ofHours(1)); localCache.put(artworkId, dbData); } return dbData; } finally { lock.unlock(); } } ``` **交付物:** - ✅ 创意作品完整的 CRUD - ✅ 缓存系统防穿透、击穿、雪崩 - ✅ 搜索功能可用 --- ## **第 3 周:消息队列 + 异步处理** **学习目标:** RabbitMQ、死信队列、消息可靠性、幂等性 **核心任务:** ```text 1. 设计 MQ 消息体(事件驱动架构) - 创意发布事件 - 拍卖结算事件 - 交易完成事件 2. 实现消息发送者和消费者 3. 配置死信队列处理异常 4. 实现幂等性处理(重复消息) 5. 消息可靠性保证(确认机制) ``` **核心事件设计:** ```java // 事件类型定义 enum EventType { ARTWORK_PUBLISHED, // 创意发布 ARTWORK_REPORTED, // 创意举报 AUCTION_STARTED, // 拍卖开始 BID_PLACED, // 出价 AUCTION_ENDED, // 拍卖结束 TRANSACTION_COMPLETED, // 交易完成 PAYMENT_SUCCESS, // 支付成功 REVENUE_SETTLED, // 收益结算 BLINDBOX_OPENED // 盲盒打开 } // 事件消息 @Data public class DomainEvent { private String eventId; // 幂等性 key private EventType eventType; private Long timestamp; private Map payload; private String source; // 来源服务 } ``` **交付物:** - ✅ MQ 系统完整接入 - ✅ 创意发布 → 通知用户 的异步流程 - ✅ 消息可靠性测试通过 --- ## **第 4 周:拍卖核心逻辑 + 实时竞价** **学习目标:** WebSocket、Sentinel 限流、复杂业务逻辑、Redis Lua 脚本 **核心任务:** ```text 1. 设计拍卖数据模型和状态机 2. 实现三种拍卖策略 - 英式拍卖(逐步递增) - 荷兰式拍卖(逐步递减) - 密封竞价(一次性出价) 3. WebSocket 实时推送竞价信息 4. Sentinel 防止恶意出价(限流) 5. Redis Lua 脚本实现原子出价操作 6. 竞价锁防止并发冲突 ``` **Lua 脚本示例:** ```lua -- bid_lock.lua -- 原子操作:检查出价有效性 + 记录出价 + 更新最高价 local auctionId = KEYS[1] local userId = KEYS[2] local bidAmount = ARGV[1] local currentMaxBid = redis.call('GET', 'auction:' .. auctionId .. ':max_bid') -- 检查出价是否有效 if tonumber(bidAmount) <= tonumber(currentMaxBid or 0) then return {0, "出价必须高于当前最高价"} end -- 检查用户余额(从钱包服务缓存) local userBalance = redis.call('GET', 'wallet:' .. userId .. ':balance') if tonumber(userBalance or 0) < tonumber(bidAmount) then return {0, "余额不足"} end -- 原子更新最高价和最高出价者 redis.call('SET', 'auction:' .. auctionId .. ':max_bid', bidAmount) redis.call('SET', 'auction:' .. auctionId .. ':max_bidder', userId) redis.call('LPUSH', 'auction:' .. auctionId .. ':bid_history', userId .. ':' .. bidAmount .. ':' .. redis.call('TIME')[1]) -- 发布出价事件到 pub/sub(WebSocket 订阅) redis.call('PUBLISH', 'auction:' .. auctionId .. ':bids', cjson.encode({userId, bidAmount, redis.call('TIME')[1]})) return {1, "出价成功"} ``` **WebSocket 推送示例:** ```java @ServerEndpoint("/ws/auction/{auctionId}") public class AuctionWebSocketServer { private static Set sessions = new CopyOnWriteArraySet<>(); private RedisTemplate redisTemplate; @OnOpen public void onOpen(@PathParam("auctionId") Long auctionId, Session session) { sessions.add(session); // 订阅 Redis Pub/Sub 频道 subscribeToAuctionChannel(auctionId); } private void subscribeToAuctionChannel(Long auctionId) { String channel = "auction:" + auctionId + ":bids"; redisTemplate.getConnectionFactory() .getConnection() .subscribe((message, pattern) -> { // 实时广播给所有连接的客户端 String bidInfo = new String(message.getBody()); sessions.forEach(session -> { try { session.getBasicRemote() .sendText(bidInfo); } catch (IOException e) { e.printStackTrace(); } }); }, channel.getBytes()); } } ``` **交付物:** - ✅ 拍卖系统完整可用 - ✅ WebSocket 实时竞价 - ✅ 支持多种拍卖策略 - ✅ 并发安全性测试通过(万级并发) --- ## **第 5 周:盲盒 + 聚合拍卖 + 订单系统** **学习目标:** 复杂业务逻辑、抽奖算法、组合交易 **核心任务:** ```text 1. 盲盒系统 - 盲盒创建和库存管理 - 奖品分布策略(概率分层) - 开箱动画数据和结果 - 防止重复开箱(幂等性) 2. 聚合拍卖(组合购买) - 选择多个创意组合 - 一次竞价多个作品 - 批量结算 3. 订单系统 - 订单创建、支付、确认 - 订单查询和历史记录 - 订单取消和退款 ``` **盲盒抽奖算法:** ```java @Service public class LotteryService { private static final Random random = new Random(); // 加权随机抽奖 public Prize drawPrize(BlindBox blindBox) { List prizes = blindBox.getPrizes(); // 计算权重总和 int totalWeight = prizes.stream() .mapToInt(Prize::getWeight) .sum(); // 随机选择 int randomValue = random.nextInt(totalWeight); int currentWeight = 0; for (Prize prize : prizes) { currentWeight += prize.getWeight(); if (randomValue < currentWeight) { return prize; } } return prizes.get(0); // 默认返回 } // 开箱逻辑(包含防重复操作) @Transactional public BoxOpenResult openBox(String userId, Long boxId) { // Redis 中记录用户是否已开过这个盒子 String openKey = "blindbox:opened:" + userId + ":" + boxId; // 使用 Lua 脚本保证原子性 Boolean alreadyOpened = redisTemplate.execute( new DefaultRedisScript<>( "if redis.call('get', KEYS[1]) then " + " return 1 " + "else " + " redis.call('set', KEYS[1], '1') " + " redis.call('expire', KEYS[1], 86400) " + " return 0 " + "end", Boolean.class ), Collections.singletonList(openKey) ); if (alreadyOpened) { throw new BusinessException("已经开过这个盲盒了"); } // 真正的抽奖逻辑 BlindBox box = blindBoxService.getById(boxId); Prize prize = drawPrize(box); // 发送 MQ 事件 eventPublisher.publish(new BoxOpenedEvent( userId, boxId, prize.getId() )); return BoxOpenResult.builder() .prizeId(prize.getId()) .prizeName(prize.getName()) .prizeImage(prize.getImage()) .rarity(prize.getRarity()) // 稀有度 .animationDelay(800) // 动画延迟(毫秒) .build(); } } ``` **聚合拍卖逻辑:** ```java @Service public class CombinedAuctionService { // 创建组合拍卖 @Transactional public CombinedAuction createCombinedAuction( List artworkIds, BigDecimal startPrice) { // 验证所有作品都存在 List artworks = artworkService.listByIds(artworkIds); if (artworks.size() != artworkIds.size()) { throw new BusinessException("部分作品不存在"); } // 创建组合拍卖记录 CombinedAuction auction = new CombinedAuction(); auction.setArtworks(artworks); auction.setStartPrice(startPrice); auction.setStatus(AuctionStatus.PENDING); auction.setCreatedTime(LocalDateTime.now()); combinedAuctionMapper.insert(auction); // 发送事件 eventPublisher.publish(new AuctionCreatedEvent(auction.getId())); return auction; } // 组合拍卖出价 @Transactional public void bidOnCombination(Long auctionId, Long userId, BigDecimal bidAmount) { CombinedAuction auction = combinedAuctionMapper .selectById(auctionId); // 检查出价金额 if (bidAmount.compareTo(auction.getCurrentPrice()) <= 0) { throw new BusinessException("出价必须高于当前价格"); } // 原子操作:更新价格 + 记录出价 // 使用 Lua 脚本 String lua = "redis.call('set', KEYS[1], ARGV[1]) " + "redis.call('set', KEYS[2], ARGV[2]) " + "return 1"; redisTemplate.execute( new DefaultRedisScript<>(lua, Long.class), Arrays.asList( "auction:price:" + auctionId, "auction:bidder:" + auctionId ), bidAmount.toPlainString(), userId.toString() ); // 发布竞价事件 eventPublisher.publish( new CombinedBidPlacedEvent(auctionId, userId, bidAmount) ); } } ``` **订单系统:** ```java @Service public class OrderService { // 创建订单(可能来自多种场景:拍卖结束、盲盒、直接购买) @Transactional public Order createOrder(OrderCreateRequest request) { Order order = new Order(); order.setUserId(request.getUserId()); order.setTotalAmount(request.getTotalAmount()); order.setStatus(OrderStatus.CREATED); order.setItems(request.getItems()); order.setCreatedTime(LocalDateTime.now()); orderMapper.insert(order); // 发送订单创建事件 eventPublisher.publish(new OrderCreatedEvent(order.getId())); return order; } // 订单支付(通过钱包扣费) @Transactional public void payOrder(Long orderId) { Order order = orderMapper.selectById(orderId); // 调用钱包服务扣费(Feign RPC) walletServiceClient.deductBalance( order.getUserId(), order.getTotalAmount() ); // 更新订单状态 order.setStatus(OrderStatus.PAID); order.setPaidTime(LocalDateTime.now()); orderMapper.updateById(order); // 发送支付成功事件 eventPublisher.publish(new PaymentSuccessEvent(order.getId())); } } ``` **交付物:** - ✅ 盲盒系统完整实现(防重复、概率分层) - ✅ 聚合拍卖支持 - ✅ 订单系统 CRUD --- ## **第 6 周:钱包系统 + 分布式事务** **学习目标:** Seata 分布式事务、TCC 模式、补偿逻辑 **核心任务:** ```text 1. 钱包系统 - 用户余额管理 - 充值和提现 - 余额锁定(下单时冻结金额) 2. Seata 分布式事务 - AT 模式:自动回滚 - TCC 模式:业务补偿 - SAGA 模式:长事务编排 3. 交易结算 - 创作者收益分成 - 平台抽成 - 税费计算 ``` **钱包服务实现:** ```java @Service @Slf4j public class WalletService { // 用户余额扣费(使用 Redis 实现) public void deductBalance(Long userId, BigDecimal amount) { String balanceKey = "wallet:balance:" + userId; String lockedKey = "wallet:locked:" + userId; // 使用 Lua 脚本保证原子性 String luaScript = "local balance = tonumber(redis.call('GET', KEYS[1])) or 0 " + "local amount = tonumber(ARGV[1]) " + "if balance >= amount then " + " redis.call('DECRBY', KEYS[1], amount) " + " return 1 " + "else " + " return 0 " + "end"; Boolean result = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Boolean.class), Collections.singletonList(balanceKey), amount.toPlainString() ); if (!result) { throw new InsufficientBalanceException("余额不足"); } } // 余额锁定(下单时冻结) public void lockBalance(Long userId, BigDecimal amount) { String lockedKey = "wallet:locked:" + userId; redisTemplate.opsForValue() .increment(lockedKey, amount.doubleValue()); } // 释放锁定的余额 public void unlockBalance(Long userId, BigDecimal amount) { String lockedKey = "wallet:locked:" + userId; redisTemplate.opsForValue() .decrement(lockedKey, amount.doubleValue()); } } ``` **Seata 分布式事务示例(TCC 模式):** ```java // 业务接口 public interface OrderTransactionService { void createOrder(Long userId, Long artworkId, BigDecimal price); } // 实现类(带 Seata 注解) @Service @Slf4j public class OrderTransactionServiceImpl implements OrderTransactionService { @Autowired private WalletServiceClient walletServiceClient; @Autowired private ArtworkServiceClient artworkServiceClient; @Autowired private OrderMapper orderMapper; // 全局事务开始 @GlobalTransactional(name = "create-order-tx") @Override public void createOrder(Long userId, Long artworkId, BigDecimal price) { log.info("开始创建订单:userId={}, artworkId={}", userId, artworkId); // Step 1: Try - 预留钱包余额 walletServiceClient.lockBalance(userId, price); // Step 2: 创建订单记录 Order order = new Order(); order.setUserId(userId); order.setArtworkId(artworkId); order.setPrice(price); order.setStatus(OrderStatus.LOCKED); orderMapper.insert(order); // Step 3: 更新创意所有者的未结算收入 artworkServiceClient.addUnsettledRevenue( artworkId, price ); // 若任何一步失败,Seata 会自动回滚所有操作 log.info("订单创建成功:orderId={}", order.getId()); } } // 钱包服务(Seata 参与方) @FeignClient(name = "wallet-service") public interface WalletServiceClient { @PostMapping("/wallet/lock-balance") void lockBalance(@RequestParam Long userId, @RequestParam BigDecimal amount); // Seata 回滚方法 @PostMapping("/wallet/lock-balance-cancel") void lockBalanceCancel(@RequestParam Long userId, @RequestParam BigDecimal amount); } // 实际钱包服务实现 @Service public class WalletServiceImpl implements WalletService { @TwoPhaseBusinessAction( name = "lockBalance", commitMethod = "commit", rollbackMethod = "rollback" ) public void lockBalance(String xid, Long branchId, Long userId, BigDecimal amount) { // Try 阶段:锁定余额 walletRepository.lockBalance(userId, amount); log.info("锁定余额成功:xid={}, userId={}, amount={}", xid, userId, amount); } // Commit 阶段:确认(无需操作,资金已锁定) public boolean commit(BusinessActionContext context) { return true; } // Rollback 阶段:回滚(释放锁定的余额) public boolean rollback(BusinessActionContext context) { Long userId = (Long) context.getActionContext("userId"); BigDecimal amount = (BigDecimal) context.getActionContext("amount"); walletRepository.unlockBalance(userId, amount); log.warn("回滚锁定余额:userId={}, amount={}", userId, amount); return true; } } ``` **收益结算逻辑:** ```java @Service @Slf4j public class RevenueSettlementService { // 结算流程:创作者收益 = 交易金额 * (1 - 平台抽成) @Transactional public void settleRevenue(Long artworkId) { Artwork artwork = artworkMapper.selectById(artworkId); BigDecimal unsettledRevenue = artwork.getUnsettledRevenue(); if (unsettledRevenue.compareTo(BigDecimal.ZERO) <= 0) { return; } // 计算创作者收益 BigDecimal platformCommission = unsettledRevenue .multiply(PLATFORM_COMMISSION_RATE); // 比如 10% BigDecimal creatorRevenue = unsettledRevenue .subtract(platformCommission); // 开始分布式事务:结算 settleToCreator(artwork.getCreatorId(), creatorRevenue); settleToPlatform(platformCommission); // 更新创意的收益统计 artwork.setSettledRevenue( artwork.getSettledRevenue().add(creatorRevenue) ); artwork.setUnsettledRevenue(BigDecimal.ZERO); artworkMapper.updateById(artwork); log.info("结算完成:artworkId={}, creatorRevenue={}, " + "platformCommission={}", artworkId, creatorRevenue, platformCommission); } private void settleToCreator(Long creatorId, BigDecimal amount) { // 发送 MQ 消息:创作者提现 eventPublisher.publish( new CreatorRevenueReadyEvent(creatorId, amount) ); } private void settleToPlatform(BigDecimal commission) { // 更新平台账户 platformAccountRepository.addCommission(commission); } } ``` **交付物:** - ✅ 钱包系统完整(充值、提现、锁定) - ✅ Seata 分布式事务集成 - ✅ 结算流程验证通过 --- ## **第 7 周:投票系统 + 定时任务** **学习目标:** XXL-Job、复杂计算任务、投票算法 **核心任务:** ```text 1. 投票系统 - 创意投票 - 权重计算(粉丝数、历史评分等) - 防止刷票 2. XXL-Job 定时任务 - 拍卖自动结算 - 热门创意排行榜更新 - 年度最佳计算 - 数据报表生成 - 过期数据清理 ``` **投票系统实现:** ```java @Service @Slf4j public class VoteService { private static final int VOTES_PER_USER = 5; // 每用户每天5票 // 用户投票 @Transactional public void vote(Long userId, Long artworkId) { // 检查用户今天是否还有投票次数 String voteKey = "vote:daily:" + userId + ":" + LocalDate.now(); Long votesUsed = redisTemplate.opsForValue() .increment(voteKey, 1); if (votesUsed > VOTES_PER_USER) { throw new VoteLimitException("每天最多投5票"); } // 设置过期时间(明天自动清除) if (votesUsed == 1) { redisTemplate.expire(voteKey, Duration.ofDays(1)); } // 检查用户是否已投过这个作品 String userVoteKey = "vote:user:" + userId + ":artwork:" + artworkId; Boolean alreadyVoted = redisTemplate.hasKey( userVoteKey ); if (alreadyVoted) { throw new BusinessException("已经投过票了"); } // 记录投票 redisTemplate.opsForValue() .set(userVoteKey, "1", Duration.ofDays(365)); // 增加作品的投票数(使用权重) BigDecimal userWeight = calculateUserWeight(userId); String artworkVoteKey = "vote:artwork:" + artworkId; redisTemplate.opsForZSet() .incrementScore(artworkVoteKey, userId.toString(), userWeight.doubleValue()); log.info("投票成功:userId={}, artworkId={}", userId, artworkId); } // 计算用户投票权重 private BigDecimal calculateUserWeight(Long userId) { User user = userService.getById(userId); // 权重 = 基础分 + 粉丝数加成 + 历史评分加成 BigDecimal baseScore = BigDecimal.ONE; BigDecimal followerBonus = BigDecimal.valueOf( user.getFollowerCount() / 100.0 // 100个粉丝 +0.01 ); BigDecimal ratingBonus = user.getHistoricalRating() .multiply(BigDecimal.valueOf(0.5)); return baseScore.add(followerBonus).add(ratingBonus); } } ``` **XXL-Job 定时任务配置:** ```java @Configuration public class XxlJobConfig { @Bean public XxlJobSpringExecutor xxlJobExecutor() { XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses( "${xxl.job.admin.addresses}" ); xxlJobSpringExecutor.setAppname( "${xxl.job.executor.appname}" ); xxlJobSpringExecutor.setIp( "${xxl.job.executor.ip}" ); xxlJobSpringExecutor.setPort( ${xxl.job.executor.port} ); xxlJobSpringExecutor.setAccessToken( "${xxl.job.accessToken}" ); xxlJobSpringExecutor.setLogPath( "${xxl.job.executor.logpath}" ); xxlJobSpringExecutor.setLogRetentionDays( ${xxl.job.executor.logretentiondays} ); return xxlJobSpringExecutor; } } ``` **具体任务实现:** ```java @Slf4j @Component public class AuctionFinalizerJob { @Autowired private AuctionService auctionService; @Autowired private RabbitTemplate rabbitTemplate; // 每小时执行一次 @XxlJob("auctionFinalizerJob") public void executeJob() { log.info("拍卖结算任务开始..."); try { // 查询所有已结束的拍卖 List endedAuctions = auctionService.findEndedAuctions(); for (Auction auction : endedAuctions) { // 发送 MQ 事件进行异步结算 AuctionSettlementEvent event = new AuctionSettlementEvent(); event.setAuctionId(auction.getId()); event.setHighestBidder( auction.getHighestBidder() ); event.setHighestBid( auction.getHighestBid() ); rabbitTemplate.convertAndSend( "auction-settlement-exchange", "settlement.key", event ); } log.info("拍卖结算任务完成,处理数量:{}", endedAuctions.size()); } catch (Exception e) { log.error("拍卖结算任务异常", e); throw new RuntimeException(e); } } } @Slf4j @Component public class HotArtworkJob { @Autowired private RedisTemplate redisTemplate; @Autowired private ArtworkMapper artworkMapper; // 每 2 小时执行一次 @XxlJob("hotArtworkJob") public void executeJob() { log.info("热门创意排行榜更新开始..."); try { // 从 Redis 获取最近 24 小时的浏览数据 Set hotArtworks = redisTemplate.opsForZSet() .reverseRange("artwork:views:24h", 0, 99); // 保存到数据库(用于展示) List hotList = new ArrayList<>(); int rank = 1; for (Object artworkId : hotArtworks) { HotArtwork hot = new HotArtwork(); hot.setArtworkId(Long.parseLong( artworkId.toString() )); hot.setRank(rank++); hot.setUpdatedTime(LocalDateTime.now()); hotList.add(hot); } hotArtworkMapper.deleteAll(); hotArtworkMapper.insertBatch(hotList); // 更新排行榜缓存 redisTemplate.opsForValue() .set("hot-artworks", hotList, Duration.ofHours(2)); log.info("热门创意排行榜更新完成"); } catch (Exception e) { log.error("热门创意排行榜更新异常", e); } } } @Slf4j @Component public class AnnualAwardCalculationJob { @Autowired private VoteService voteService; @Autowired private ArtworkService artworkService; // 每年 1 月 1 日凌晨执行 @XxlJob("annualAwardCalculationJob") public void executeJob() { log.info("年度最佳创意计算开始..."); try { LocalDateTime startOfYear = LocalDateTime.of( LocalDate.now().getYear(), 1, 1, 0, 0, 0 ); LocalDateTime endOfYear = LocalDateTime.now(); // 查询去年发布的所有作品 List artworks = artworkService.listByCreatedBetween( startOfYear, endOfYear ); // 计算综合得分(投票 + 销售 + 热度) Map scoreMap = new HashMap<>(); for (Artwork artwork : artworks) { BigDecimal voteScore = voteService.getArtworkVoteScore( artwork.getId() ); BigDecimal salesScore = artwork.getTotalSales() .divide(BigDecimal.valueOf(1000)); BigDecimal viewScore = BigDecimal.valueOf( artwork.getViewCount() / 100.0 ); BigDecimal totalScore = voteScore .multiply(BigDecimal.valueOf(0.4)) .add(salesScore .multiply(BigDecimal.valueOf(0.35))) .add(viewScore .multiply(BigDecimal.valueOf(0.25))); scoreMap.put(artwork.getId(), totalScore); } // 获取前 10 名 List> topTen = scoreMap.entrySet().stream() .sorted((a, b) -> b.getValue().compareTo(a.getValue()) ) .limit(10) .collect(Collectors.toList()); // 保存年度最佳 AnnualAward award = new AnnualAward(); award.setYear(LocalDate.now().getYear()); award.setTopArtworks(topTen.stream() .map(Map.Entry::getKey) .collect(Collectors.toList())); award.setCreatedTime(LocalDateTime.now()); annualAwardMapper.insert(award); log.info("年度最佳创意计算完成:{}", topTen); } catch (Exception e) { log.error("年度最佳创意计算异常", e); } } } ``` **交付物:** - ✅ 投票系统完整(防刷票、权重计算) - ✅ XXL-Job 多个任务实现 - ✅ 排行榜和年度最佳计算 --- ## **第 8 周:监控 + 上线部署** **学习目标:** Skywalking 链路追踪、Docker 部署、性能优化 **核心任务:** ```text 1. Skywalking 链路追踪 - 服务间调用可视化 - 性能瓶颈识别 - 日志关联 2. Docker 容器化部署 - 每个微服务编写 Dockerfile - 编写 docker-compose.yml - 环境变量管理 3. 性能优化 - 数据库索引优化 - 缓存热点数据 - 异步处理优化 ``` **Dockerfile 示例:** ```dockerfile # 多阶段构建 FROM maven:3.8-openjdk-11 as builder WORKDIR /app COPY . . RUN mvn clean package -DskipTests # 最终镜像 FROM openjdk:11-jre-slim WORKDIR /app COPY --from=builder /app/target/*.jar app.jar # Skywalking 代理 ADD https://archive.apache.org/dist/skywalking/8.9.0/apache-skywalking-apm-8.9.0.tar.gz . RUN tar -xzf apache-skywalking-apm-8.9.0.tar.gz ENV JAVA_OPTS="-javaagent:/app/skywalking/agent/skywalking-agent.jar" \ SW_AGENT_NAME="canvas-chain-service" \ SW_AGENT_COLLECTOR_BACKEND_SERVICES="skywalking-oap:11800" EXPOSE 8080 ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] ``` **docker-compose.yml 完整编排:** ```yaml version: '3.8' services: # MySQL 数据库 mysql: image: mysql:8.0 container_name: canvas-mysql environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: canvas_chain ports: - "3306:3306" volumes: - ./docs/init.sql:/docker-entrypoint-initdb.d/init.sql - mysql-data:/var/lib/mysql networks: - canvas-network # Redis 缓存 redis: image: redis:7-alpine container_name: canvas-redis ports: - "6379:6379" command: redis-server --appendonly yes --requirepass canvas123456 volumes: - redis-data:/data networks: - canvas-network # RabbitMQ 消息队列 rabbitmq: image: rabbitmq:3.12-management-alpine container_name: canvas-rabbitmq environment: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest ports: - "5672:5672" - "15672:15672" volumes: - rabbitmq-data:/var/lib/rabbitmq networks: - canvas-network # Nacos 服务注册中心 nacos: image: nacos/nacos-server:v2.2.0 container_name: canvas-nacos environment: MODE: standalone NACOS_AUTH_ENABLE: "false" ports: - "8848:8848" - "9848:9848" volumes: - nacos-data:/home/nacos/data networks: - canvas-network # ElasticSearch 搜索引擎 elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0 container_name: canvas-es environment: - discovery.type=single-node - xpack.security.enabled=false - "ES_JAVA_OPTS=-Xms512m -Xmx512m" ports: - "9200:9200" volumes: - es-data:/usr/share/elasticsearch/data networks: - canvas-network # Kibana ES 可视化 kibana: image: docker.elastic.co/kibana/kibana:8.5.0 container_name: canvas-kibana environment: ELASTICSEARCH_HOSTS: "http://elasticsearch:9200" ports: - "5601:5601" depends_on: - elasticsearch networks: - canvas-network # Skywalking OAP 服务器(链路追踪) skywalking-oap: image: apache/skywalking-oap-server:8.9.0 container_name: canvas-skywalking-oap environment: SW_STORAGE: elasticsearch SW_ELASTICSEARCH_REPLICAS: 0 SW_ELASTICSEARCH_PEERS: "elasticsearch:9200" ports: - "11800:11800" - "12800:12800" depends_on: - elasticsearch networks: - canvas-network # Skywalking UI skywalking-ui: image: apache/skywalking-ui:8.9.0 container_name: canvas-skywalking-ui ports: - "8080:8080" environment: SW_OAP_ADDRESS: "skywalking-oap:12800" depends_on: - skywalking-oap networks: - canvas-network # Seata Server 分布式事务协调器 seata-server: image: seataio/seata-server:latest container_name: canvas-seata-server environment: SEATA_PORT: 8091 SEATA_CONFIG_NAME: file:/seata-config/registry.conf ports: - "8091:8091" volumes: - ./config/seata/registry.conf:/seata-config/registry.conf - seata-data:/seata-server/sessionStore networks: - canvas-network # XXL-Job 定时任务平台 xxl-job-admin: image: xuxueli/xxl-job-admin:2.4.0 container_name: canvas-xxl-job-admin environment: PARAMS: "--server.servlet.context-path=/xxl-job-admin --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root123456" ports: - "8081:8080" depends_on: - mysql networks: - canvas-network # Sentinel 限流控制台 sentinel-dashboard: image: bladex/sentinel-dashboard:latest container_name: canvas-sentinel-dashboard ports: - "8858:8858" networks: - canvas-network # 网关服务(基于 Spring Cloud Gateway) gateway: build: context: ./canvas-chain-gateway dockerfile: Dockerfile container_name: canvas-gateway environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-gateway" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" ports: - "9000:8080" depends_on: - nacos - redis - skywalking-oap networks: - canvas-network # 用户服务 user-service: build: context: ./canvas-chain-user-service dockerfile: Dockerfile container_name: canvas-user-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-user-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" ports: - "8001:8080" depends_on: - mysql - redis - nacos - skywalking-oap networks: - canvas-network # 创意服务 artwork-service: build: context: ./canvas-chain-artwork-service dockerfile: Dockerfile container_name: canvas-artwork-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-artwork-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" ELASTICSEARCH_HOSTS: "http://elasticsearch:9200" ports: - "8002:8080" depends_on: - mysql - redis - nacos - skywalking-oap - elasticsearch networks: - canvas-network # 拍卖服务 auction-service: build: context: ./canvas-chain-auction-service dockerfile: Dockerfile container_name: canvas-auction-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-auction-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" RABBITMQ_USER: "guest" RABBITMQ_PASSWORD: "guest" ports: - "8003:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap networks: - canvas-network # 盲盒服务 blindbox-service: build: context: ./canvas-chain-blindbox-service dockerfile: Dockerfile container_name: canvas-blindbox-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-blindbox-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" ports: - "8004:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap networks: - canvas-network # 订单服务 order-service: build: context: ./canvas-chain-order-service dockerfile: Dockerfile container_name: canvas-order-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-order-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" SEATA_SERVER: "seata-server:8091" ports: - "8005:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap - seata-server networks: - canvas-network # 钱包服务 wallet-service: build: context: ./canvas-chain-wallet-service dockerfile: Dockerfile container_name: canvas-wallet-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-wallet-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" SEATA_SERVER: "seata-server:8091" ports: - "8006:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap - seata-server networks: - canvas-network # 投票服务 vote-service: build: context: ./canvas-chain-vote-service dockerfile: Dockerfile container_name: canvas-vote-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-vote-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" ports: - "8007:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap networks: - canvas-network # NFT 服务 nft-service: build: context: ./canvas-chain-nft-service dockerfile: Dockerfile container_name: canvas-nft-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-nft-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" RABBITMQ_HOST: "rabbitmq" ports: - "8008:8080" depends_on: - mysql - redis - nacos - rabbitmq - skywalking-oap networks: - canvas-network # 定时任务服务 job-service: build: context: ./canvas-chain-job-service dockerfile: Dockerfile container_name: canvas-job-service environment: JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar" SW_AGENT_NAME: "canvas-job-service" SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800" NACOS_SERVER_ADDR: "nacos:8848" MYSQL_HOST: "mysql" MYSQL_USER: "root" MYSQL_PASSWORD: "root123456" REDIS_HOST: "redis" REDIS_PASSWORD: "canvas123456" XXL_JOB_ADMIN_ADDRESSES: "http://xxl-job-admin:8080/xxl-job-admin" ports: - "8009:8080" depends_on: - mysql - redis - nacos - skywalking-oap - xxl-job-admin networks: - canvas-network volumes: mysql-data: redis-data: rabbitmq-data: nacos-data: es-data: seata-data: networks: canvas-network: driver: bridge ``` ## 🚀 **快速启动指南** ```bash # 1. 克隆项目 git clone https://github.com/your-username/canvas-chain.git cd canvas-chain # 2. 启动所有容器 docker-compose up -d # 3. 查看日志 docker-compose logs -f # 4. 初始化数据库 docker exec canvas-mysql mysql -uroot -proot123456 < docs/init.sql # 5. 访问各服务 # API 网关: http://localhost:9000 # Nacos: http://localhost:8848/nacos (user/nacos) # XXL-Job: http://localhost:8081/xxl-job-admin (admin/123456) # Skywalking: http://localhost:8080 # Kibana: http://localhost:5601 # RabbitMQ: http://localhost:15672 (guest/guest) ``` --- ## 📊 **性能优化建议** ### **1. 数据库优化** ```sql -- 创意表索引优化 CREATE INDEX idx_creator_id ON artwork(creator_id); CREATE INDEX idx_category ON artwork(category); CREATE INDEX idx_created_time ON artwork(created_time); CREATE INDEX idx_status ON artwork(status); -- 拍卖表索引 CREATE INDEX idx_auction_status ON auction(status); CREATE INDEX idx_auction_end_time ON auction(end_time); -- 交易表索引 CREATE INDEX idx_transaction_user ON transaction(user_id); CREATE INDEX idx_transaction_created ON transaction(created_time); ``` ### **2. Redis 热点数据** ```java @Service public class CacheWarmupService { @Scheduled(cron = "0 0 * * * *") // 每小时执行 public void warmupHotData() { // 预热热门创意 List hotArtworks = artworkService .listHotArtworks(); for (Artwork artwork : hotArtworks) { String key = "artwork:" + artwork.getId(); redisTemplate.opsForValue() .set(key, artwork, Duration.ofHours(1)); } // 预热排行榜 redisTemplate.opsForValue() .set("ranking:daily", getRankingData(), Duration.ofHours(1)); } } ``` ### **3. 异步处理优化** ```java @Configuration @EnableAsync public class AsyncConfig { @Bean(name = "taskExecutor") public ThreadPoolTaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(1000); executor.setThreadNamePrefix("canvas-async-"); executor.initialize(); return executor; } } // 使用 @Service public class AuctionService { @Async("taskExecutor") public void processAuctionSettlement(Long auctionId) { // 耗时操作 } } ``` --- ## 📈 **监控指标** ### **关键指标监控** ```java @Component @Slf4j public class MetricsCollector { @Autowired private MeterRegistry meterRegistry; // 记录拍卖竞价 public void recordBid(Long auctionId, BigDecimal bidAmount) { meterRegistry.counter( "auction.bid.count", "auctionId", auctionId.toString() ).increment(); meterRegistry.gauge( "auction.bid.amount", bidAmount.doubleValue() ); } // 记录交易 public void recordTransaction(Long userId, BigDecimal amount) { meterRegistry.counter( "transaction.count", "userId", userId.toString() ).increment(); meterRegistry.timer("transaction.duration") .record(Duration.ofSeconds(1)); } // 记录缓存命中率 public void recordCacheHit(String cacheName, boolean hit) { meterRegistry.counter( "cache.hits", "cache", cacheName, "hit", String.valueOf(hit) ).increment(); } } ``` --- ## 🎯 **最终交付清单** **8周后你将得到:** | **阶段 (Week)** | **完成内容 (Deliverables)** | **技术栈 (Tech Stack)** | | --- | --- | --- | | **W1** | 微服务架构搭建:项目脚手架、服务注册与发现、数据库初始化 | Nacos、Spring Cloud Alibaba、Spring Boot、MySQL | | **W2** | 创意服务开发 + 多级缓存:作品 CRUD、缓存穿透/击穿防护、搜索引擎接入 | Redis、Caffeine、本地缓存、布隆过滤器、ElasticSearch | | **W3** | 消息队列系统:事件驱动设计、死信队列、消息幂等性保证 | RabbitMQ、Spring AMQP、消息模型设计 | | **W4** | 拍卖核心逻辑:实时竞价、限流、原子性出价脚本 | WebSocket、Sentinel、Redis Lua、并发锁 | | **W5** | 盲盒系统 + 订单管理:加权抽奖、库存管理、订单全流程 | 抽奖算法、幂等设计、订单状态机、事务管理 | | **W6** | 钱包系统 + 分布式事务:扣费、锁定、收益结算、跨服务一致性 | Seata、TCC 模式、AT 模式、补偿机制 | | **W7** | 投票系统 + 定时任务:权重投票、排行榜、自动化任务调度 | XXL-Job、投票算法、排行榜计算 | | **W8** | 监控 + 上线部署:链路追踪、容器化、自动化部署流程 | SkyWalking、Docker、docker-compose、CI/CD | **代码统计:** - ✅ 总代码行数:**~50,000+ 行** - ✅ 微服务数量:**10 个** - ✅ 数据库表:**30+ 张** - ✅ API 接口:**200+ 个** - ✅ 中间件集成:**全栈** **简历亮点:** - ✅ 完整的分布式系统设计经验 - ✅ 中间件全栈掌握(MQ、缓存、注册中心、限流、事务、任务调度) - ✅ 高并发场景实战经验 - ✅ 企业级代码规范和架构设计能力 - ✅ 可部署的生产级系统 --- **VibeCoding 导航**:⬅️ [[02-CanvasChain 第1周 - 基础架构搭建详细设计文档|02-CanvasChain 第1周 - 基础架构搭建详细设计文档]] | 01-计划 | ➡️ [[02-CanvasChain 项目 8 周计划可行性分析|02-CanvasChain 项目 8 周计划可行性分析]]